"""
GUI 地面站客户端

date: 2025/9/25
author: SiHeng Tang
file: gui-client.py
copyright(c) DFSA Eastern Software
此程序不能提供任何担保 WITHOUT WARRANTY OF ANY KIND
"""
from __future__ import annotations

import json
import logging
import math
import os.path
import queue
import sys

import cv2
from PySide6 import QtGui, QtCore
from PySide6.QtCore import Qt, Slot, QEvent, QObject, Signal
from PySide6.QtGui import QPixmap, QKeyEvent
from PySide6.QtWidgets import QApplication, QDialog, QLabel, QTableWidgetItem, QMainWindow
from PySide6.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsPixmapItem

import client_core
from client_core import MJWebAPIClient, _parse_command
from lib_gui_client import (cv2q_image, TargetsSet, Ui_connections_dialog,
                            JsonModel, START_UP, map_fm,
                            MapView)
from main_win import Ui_MainWindow

logger = logging.getLogger(__name__)


class MultiSelectionFilter(QObject):
    """
    快速目标确认实现
    """
    ctl_keys_map = {Qt.Key.Key_Left: "⬅️", Qt.Key.Key_Right: "➡️", Qt.Key.Key_Up: "⬆️", Qt.Key.Key_Down: "⬇️"}
    dig_map: dict[int, tuple[Qt.Key, ...]] = {
        0: (Qt.Key.Key_Up, Qt.Key.Key_Up, Qt.Key.Key_Up),
        1: (Qt.Key.Key_Up, Qt.Key.Key_Up, Qt.Key.Key_Down),
        2: (Qt.Key.Key_Up, Qt.Key.Key_Up, Qt.Key.Key_Left),
        3: (Qt.Key.Key_Up, Qt.Key.Key_Up, Qt.Key.Key_Right),
        4: (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Up),
        5: (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Down),
        6: (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Left),
        7: (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Right),
        8: (Qt.Key.Key_Up, Qt.Key.Key_Left, Qt.Key.Key_Up),
        9: (Qt.Key.Key_Up, Qt.Key.Key_Left, Qt.Key.Key_Down),
    }
    target_selected = Signal()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.signal_mode = False

        self.app: MengjiGCSApp | None = None
        self.fast_key_to_targets_id = {}
        self.fast_select_seq_cache: list = []
        self.available_fast_selection_seq: list[list] = []
        self.selected_target = False
        self.in_fast_select = False

        self._target_id: int | None = None

    @property
    def get_id(self):
        """
        目标 id
        :return: 选中则为非负 ID
        """
        if self._target_id is not None:
            return self._target_id
        else:
            return -1

    def set_app(self, _app: MengjiGCSApp):
        self.app = _app
        self.app.targets_set.new_target.connect(self.update_fast_key_seq)

    def eventFilter(self, obj: QObject, event: QKeyEvent):
        """
        事件处理
        :param obj: 事件处理控件，使用此参数区别独立事件
        :param event: 事件对象，实际上有多种，这里直接设为 KeyEvent
        """
        # 触发处理条件，键盘事件，触发模式，事件去重 (qt feature)
        if (event.type() == QEvent.Type.KeyPress
                and not (self.signal_mode and self.selected_target)
                and type(obj) == QtGui.QWindow):

            key_event = QKeyEvent(event)

            if (key_event.key() in self.ctl_keys_map) and (not self.in_fast_select):
                logger.warning(f"press {key_event.key()} active fast select")
                self.fast_select_seq_cache.append(key_event.key())
                self.in_fast_select = True

            elif key_event.key() == Qt.Key.Key_Escape and self.in_fast_select:
                logger.warning(f"press {key_event.key()} exit fast select")
                self.fast_select_seq_cache.clear()
                self.in_fast_select = False

            elif (key_event.key() in self.ctl_keys_map) and self.in_fast_select:
                logger.warning(f"fast key {key_event.key()}")
                self.fast_select_seq_cache.append(key_event.key())

            key_chars = [self.ctl_keys_map[it] for it in self.fast_select_seq_cache]
            self.app.select_target_button.setText(_k := "".join(key_chars))

            if self.fast_select_seq_cache in self.available_fast_selection_seq:
                self.fast_select_seq_cache.clear()
                self.app.select_target_button.setText(f"Select Target {self.fast_key_to_targets_id[_k]}")
                self._target_id = self.fast_key_to_targets_id[_k]
                logger.warning(f"select {self.fast_key_to_targets_id[_k]}")
                self.selected_target = True
                self.target_selected.emit()

        return QObject.eventFilter(self, obj, event)

    @Slot()
    def update_fast_key_seq(self):
        """
        当目标列表更新时，重新生成键映射
        """
        self.available_fast_selection_seq.clear()

        for target_id in self.app.targets_set.target_pids:
            fast_key_seq = self.int_list_to_key_sequences(target_id)
            key_chars = [self.ctl_keys_map[it] for it in fast_key_seq]
            self.app.targets_set.set_fast_key_seq_str(target_id, "".join(key_chars))

            self.available_fast_selection_seq.append(fast_key_seq)
            self.fast_key_to_targets_id.setdefault("".join(key_chars), target_id)

        logger.warning(f"available fast select {self.available_fast_selection_seq}")

    @staticmethod
    def int_list_to_key_sequences(num: int) -> list[Qt.Key]:
        """
        将整数列表转换为按键序列二维列表
        :param num: 整数
        :return: Qt 键盘值列表
        """
        if num == 0:
            return list(MultiSelectionFilter.dig_map[0])
        digits = []
        n = num
        while n > 0:
            digits.append(n % 10)
            n = n // 10
        sequence = []
        for digit in digits:
            sequence.extend(MultiSelectionFilter.dig_map[digit])
        return sequence


class MengjiGCSApp(QApplication, Ui_MainWindow):
    """
    地面站图形应用程序类，处理所有的图形事件，绑定所有控件
    """
    config_path = "./gcsrc.json"
    connected_to_fc = Signal()
    new_map_view_image = Signal()

    def __init__(self):
        QApplication.__init__(self, sys.argv)
        Ui_MainWindow.__init__(self)

        # 遥测系统
        self._ci_core: MJWebAPIClient | None = None
        self._ci_timer = QtCore.QTimer(self)
        self._ci_timer.timeout.connect(self.check_rpc_result)
        self._message_handler: dict = {}

        # 初始化客户端数据容器
        self.targets_set = TargetsSet()
        self.mj_config_json = JsonModel()
        self._last_config_json_obj = dict()

        # 尝试加载配置文件
        if os.path.exists(self.config_path):
            logger.info(f"found config")
            with open(self.config_path, "r") as cfg_f:
                self.gcs_config = json.load(cfg_f)
        else:
            logger.warning("no config file, using default")
            self.gcs_config = {}

        self.mw = QMainWindow()
        self.setupUi(self.mw)
        self.map_view = MapView()

        # 额外的目标位置显示面板控件初始化，新建画布，设置抗锯齿，拖动模式，注入滚轮缩放回调
        self.position_scene = QGraphicsScene(self.mw)
        self.targets_position.setScene(self.position_scene)
        self.targets_position.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
        self.targets_position.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
        self.targets_position.wheelEvent = self.target_position_view_wheel_cb
        self.base_mouse_press = self.targets_position.mousePressEvent
        self.targets_position.mousePressEvent = self.target_position_on_clicked

        self.targets_table.setColumnCount(4)
        self.targets_table.setHorizontalHeaderLabels(["Index", "Pics", "AI Num", "Fast Key"])

        self.mj_config_view.setModel(self.mj_config_json)

        # 信号槽绑定
        self.select_target_button.clicked.connect(self.select_target)
        self.abort_button_a.clicked.connect(self.abort_a)
        self.abort_button_b.clicked.connect(self.abort_b)
        self.action_connection.triggered.connect(self.start_connection)
        self.action_restart_mj.triggered.connect(self.mj_software_reset)
        self.action_download_mj_config.triggered.connect(self.download_mj_config)
        self.action_upload_mj_config.triggered.connect(self.upload_mj_config)
        self.action_lock_payload.triggered.connect(self.lock_payload)
        self.action_install_payload.triggered.connect(self.install_payload)
        self.action_about.triggered.connect(self.about)
        self.actionAbout_Qt.triggered.connect(self.aboutQt)
        self.send_button.clicked.connect(self.commit_cli_command)
        self.command_line.returnPressed.connect(self.commit_cli_command)

        self.targets_set.new_target_fast_key.connect(self.update_target_analyse_pics)
        self.targets_set.new_target.connect(self.update_target_analyse_pics)
        self.targets_set.new_target.connect(self.update_target_map_view)

        self.mj_config_json.data_modified.connect(self.update_json_raw_text)
        self.mj_config_text.textChanged.connect(self.update_json_rendered_model)

        self.connected_to_fc.connect(self.init_log_print)
        self.connected_to_fc.connect(self.init_pfd_topic)
        self.connected_to_fc.connect(self.init_target_scanner)

        self.new_map_view_image.connect(self.map_view_update)
        self.update_map_view.clicked.connect(self.map_view_update)
        self.enable_view_tx.clicked.connect(self.modify_topic_target_scanner)

        self.mks_filter = MultiSelectionFilter(self)
        self.mks_filter.set_app(self)
        self.installEventFilter(self.mks_filter)
        self.mks_filter.target_selected.connect(self.select_target)

        self.is_abort_a_triggered = False

    def mainloop(self):
        """
        事件循环
        """
        self.mw.show()
        sys.exit(self.exec())

    def __test__(self):
        # 测试例程，加载图像和模拟目标
        self._set_target_position_image(cv2.imread("../../docs/0dd0a83d-2025-09-21_15-44-18_000027.jpeg"))

        self.targets_set.add_target(0, cv2.imread("../../docs/target0_screenshot_25.09.2025.png"),
                                    31.9396400, 118.7822880)
        self.targets_set.add_target(1, cv2.imread("../../docs/target1_screenshot_25.09.2025.png"),
                                    31.9397400, 118.7823880)
        self.targets_set.add_target(2, cv2.imread("../../docs/target2_screenshot_25.09.2025.png"),
                                    31.9399400, 118.7824880)
        self.targets_set.add_target(3, cv2.imread("../../docs/target3_screenshot_25.09.2025.png"),
                                    31.9396400, 118.7825880)

        with open("../../config_examples/web-api-test.json", "r") as f:
            self.mj_config_json.load(json.load(f))

    ###########################
    # UI 设置方法，展示基本数据
    ###########################
    def _set_target_position_image(self, img):
        """
        设置目标位置区图像
        :param img: 图像
        """
        if img is not None:
            it = QGraphicsPixmapItem(QtGui.QPixmap.fromImage(cv2q_image(img)))

            self.position_scene.clear()
            self.position_scene.addItem(it)

    def _set_plane_attitude(self, roll: float, pitch: float):
        self.pitch_attitude.setValue(int(pitch))
        self.roll_attitude.setValue(int(roll))

    def _set_global_position(self, altitude: float, lat: float, lon: float, velocity: float):
        self.altitude_label.setText(f"relative {altitude} m")
        self.gps_longitude_label.setText(f"longitude {lon} Deg")
        self.gps_latitude_label.setText(f"latitude {lat} Deg")
        self.speed_lcd.display(velocity)

    def _set_flight_mode(self, fm_char: str):
        self.fm_label.setText(fm_char)

    def _set_weapon_status(self, is_released: bool):
        if is_released:
            self.is_wepon_active.setText("RELEASED")
        else:
            self.is_wepon_active.setText("READY")

    ############################
    # 遥测处理
    ############################
    @Slot()
    def init_pfd_topic(self):
        if not self._ci_core:
            return

        _id = self._ci_core.commit_a_request("status/fm", "subscribe")
        self._message_handler.setdefault(_id, self.handle_pfd_result)
        _id = self._ci_core.commit_a_request("status/attitude", "subscribe")
        self._message_handler.setdefault(_id, self.handle_pfd_result)
        _id = self._ci_core.commit_a_request("status/gps", "subscribe")
        self._message_handler.setdefault(_id, self.handle_pfd_result)

    @Slot()
    def init_target_scanner(self):
        if not self._ci_core:
            return

        # 包含了光学扫描结果回传和任务控制目标回传
        if self.enable_view_tx.isChecked():
            _id = self._ci_core.commit_a_request("scanner/overview", "subscribe")
            self._message_handler.setdefault(_id, self.handle_scanner)
        _id = self._ci_core.commit_a_request("mg/lsttgt", "subscribe")
        self._message_handler.setdefault(_id, self.handle_target_topic)

    @Slot()
    def modify_topic_target_scanner(self):
        if not self.enable_view_tx.isChecked():
            self.statusbar.showMessage("Disable map view tx.", 5000)
            self._ci_core.commit_a_request("scanner/overview", "unsubscribe")
        else:
            _id = self._ci_core.commit_a_request("scanner/overview", "subscribe")
            self._message_handler.setdefault(_id, self.handle_scanner)
            self.statusbar.showMessage("Enable map view tx.", 5000)

    @Slot()
    def init_log_print(self):
        if not self._ci_core:
            return

        _id = self._ci_core.commit_a_request("api/logging", "subscribe")
        self._message_handler.setdefault(_id, self.handle_log_result)

    def handle_log_result(self, _r: dict):
        _r = _r["result"]
        if not _r:
            return

        for l in _r:
            self.console_display.moveCursor(QtGui.QTextCursor.MoveOperation.End, QtGui.QTextCursor.MoveMode.MoveAnchor)
            self.console_display.insertPlainText(f"[SYS]: {l}\n")
            self.console_display.moveCursor(QtGui.QTextCursor.MoveOperation.End, QtGui.QTextCursor.MoveMode.MoveAnchor)

    def handle_pfd_result(self, _r: dict):
        _r = _r["result"]
        if not _r:
            return

        if "hdg" in _r:
            self._set_global_position(_r["relative_alt"], _r["lat"], _r["lon"],
                                      math.sqrt(_r["vx"] ** 2 + _r["vz"] ** 2 + _r["vy"] ** 2))
        elif "yaw" in _r:
            self._set_plane_attitude(_r["roll"], _r["pitch"])
        elif "base_mode" in _r:
            self._set_flight_mode(map_fm(_r["custom_mode"]))

    def handle_scanner(self, _r: dict):
        _r = _r["result"]
        if not _r:
            return

        img = self.map_view.add_position_img(_r)

        if self.is_directly_cam_out.isChecked() and self.is_osd_cam.isChecked():
            self._set_target_position_image(img)

        if self.is_osd_cam.isChecked() and not self.is_directly_cam_out.isChecked():
            self.new_map_view_image.emit()

    def handle_target_topic(self, _r: dict):
        _r = _r["result"]
        if not _r:
            return

        for t in _r:
            is_not_exist = self.targets_set.add_target(t[0], None, t[1], t[2])
            if is_not_exist:
                self.statusbar.showMessage(f"New target with id: {t[0]}")

    ############################
    # 事件回调
    ############################
    @Slot()
    def map_view_update(self):
        # 用户可以控制图像区是否刷新，叠加显示的图像数量
        str_cnt = self.view_rander_length.currentText()
        self._set_target_position_image(self.map_view.get_img(int(str_cnt)))

    def target_position_view_wheel_cb(self, event: QtGui.QWheelEvent):
        """
        实现鼠标滚轮缩放
        """
        factor = 1.05
        if event.angleDelta().y() < 0:
            factor = 0.95
        self.targets_position.scale(factor, factor)

    def target_position_on_clicked(self, event: QtGui.QMouseEvent):
        """
        捕获鼠标点击事件，推算点击在原始图像上的坐标
        """
        self.base_mouse_press(event)

        if event.button() == QtCore.Qt.MouseButton.RightButton:
            scene_pos = self.targets_position.mapToScene(event.position().toPoint()).toPoint()
            logger.warning(f"mouse pos {scene_pos.x()} {scene_pos.y()}")
            self.pos_display_lat.setText(f"{scene_pos.x()} lat")
            self.pos_display_lon.setText(f"{scene_pos.x()} lon")

        elif event.button() == QtCore.Qt.MouseButton.MiddleButton:
            if it := self.position_scene.items()[0]:
                self.targets_position.fitInView(it, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
                self.targets_position.centerOn(it)

    @Slot()
    def update_target_map_view(self):
        """
        更新地图目标同步
        """
        self.map_view.set_target_positions(self.targets_set.positions_for_view)

    @Slot()
    def update_target_analyse_pics(self):
        """
        刷新目标列表
        """
        targets = self.targets_set

        self.targets_table.setRowCount(len(targets.target_pids))

        for idx, target_id in enumerate(targets.target_pids):
            cv_image, _, _, fast_key_str = targets.get_target_obj_by_pid(target_id)

            # 对于预设的目标可能没有图像，忽略这类问题
            if cv_image is not None:
                pic_label = QLabel(self.targets_table)
                pic_label.setPixmap(QPixmap.fromImage(cv2q_image(cv_image)))
                pic_label.setScaledContents(True)
                self.targets_table.setCellWidget(idx, 1, pic_label)

            self.targets_table.setItem(idx, 0, QTableWidgetItem(f"<{target_id}>"))
            self.targets_table.setItem(idx, 2, QTableWidgetItem(f"NUM: "))
            self.targets_table.setItem(idx, 3, QTableWidgetItem(fast_key_str))

        self.targets_table.resizeColumnsToContents()

    @Slot(dict)
    def update_json_raw_text(self, json_obj: dict):
        """
        从 json 模型同步到文本编辑器
        :param json_obj: 当前的 json 字典对象，使用此参数防止递归调用
        """
        if json_obj != self._last_config_json_obj:
            self._last_config_json_obj = json_obj
            cfg_obj = self.mj_config_json.to_json()
            plain_text = json.dumps(cfg_obj, indent=4)
            self.mj_config_text.setText(plain_text)

    @Slot()
    def update_json_rendered_model(self):
        """
        从文本编辑器同步到模型
        """
        try:
            raw_text = self.mj_config_text.toPlainText()
            cfg_obj = json.loads(raw_text)

        # 忽略输入未完成时不能正常解析的文本
        except json.decoder.JSONDecodeError:
            pass

        else:
            self._last_config_json_obj = cfg_obj
            self.mj_config_json.load(cfg_obj)

    @Slot()
    def commit_cli_command(self):
        """
        检查指令输入并提交到核心客户端
        :return:
        """
        self.console_display.moveCursor(QtGui.QTextCursor.MoveOperation.End, QtGui.QTextCursor.MoveMode.MoveAnchor)
        str_command = self.command_line.text()
        self.command_line.clear()
        self.console_display.insertPlainText(str_command + "\n")
        self.console_display.moveCursor(QtGui.QTextCursor.MoveOperation.End, QtGui.QTextCursor.MoveMode.MoveAnchor)

        try:
            _is_msg, _mtd, _params = _parse_command(str_command)
        except RuntimeError as err:
            self.console_display.insertPlainText(str(err))
            self.console_display.moveCursor(QtGui.QTextCursor.MoveOperation.End, QtGui.QTextCursor.MoveMode.MoveAnchor)
        else:
            _msg_id = self._ci_core.commit_a_request(_mtd, _params, _is_msg)
            logger.debug(f"{_msg_id} {_mtd} {_params}")

    @Slot()
    def select_target(self):
        """
        处理目标选择结果
        """
        if self.mks_filter.get_id >= 0:
            self._ci_core.commit_a_request("mg/slctgt", self.mks_filter.get_id)
            self.statusbar.showMessage(f"Select target {self.mks_filter.get_id}")

    @Slot()
    def abort_a(self):
        """
        任务终端 A 键
        """
        logger.warning(f"trigger abort a")
        self.is_abort_a_triggered = True
        self.abort_button_a.setText("TRIGGERED")
        self.abort_button_b.setText("CLICK TO ABORT")

    @Slot()
    def abort_b(self):
        """
        任务终端 B 键，只有按照顺序按下 AB 键才能中断任务
        """
        if self.is_abort_a_triggered and self._ci_core:
            logger.warning(f"abort task")
            self._ci_core.commit_a_request("mg/rlspld", [0, 0])
        else:
            logger.warning(f"abort a not triggered, abort is locked")

    @Slot()
    def download_mj_config(self):
        """
        从飞行器下载配置文件
        """
        logger.warning("downloading mj config")
        _id = self._ci_core.commit_a_request("config", [])
        self._message_handler.setdefault(_id, self.handle_config)

    def handle_config(self, _r: dict):
        self.mj_config_json.load(_r["result"])
        self._message_handler.pop(_r["id"])

    @Slot()
    def upload_mj_config(self):
        """
        将飞行器配置文件上传
        """
        logger.warning("upload mj config")

    @Slot()
    def install_payload(self):
        """
        移动载荷管理到释放位置以装载荷载
        """
        if self._ci_core is None:
            return

        self.statusbar.showMessage("set servo at released position", 5000)
        self._ci_core.commit_a_request("payload/set", 1, True)

    @Slot()
    def lock_payload(self):
        """
        锁定载荷，手动操作方法，不触发状态机
        """
        if self._ci_core is None:
            return

        self.statusbar.showMessage("set servo at locked position", 5000)
        self._ci_core.commit_a_request("payload/set", 0, True)

    @Slot()
    def preference(self):
        """
        偏好设置，估计 2025 年不会实现了
        """
        logger.warning("open preference settings")

    @Slot()
    def about(self):
        """
        关于此软件页面
        """
        logger.warning("MJ Ground Station @ DFSA Software Solutions")

    @Slot()
    def mj_software_reset(self):
        """
        MJ 空侧软件重置
        """
        self.statusbar.showMessage("send mj software reset command", 5000)
        self._ci_core.commit_a_request("api/restart_mj", [], True)

    @Slot()
    def start_connection(self):
        """
        启动连接
        """
        dialog = QDialog()
        dialog_ui = Ui_connections_dialog()
        dialog_ui.setupUi(dialog)
        dialog_ui.com_list.insertItems(0, self.gcs_config.keys())

        val = dialog.exec()

        if val == 0:
            return

        config_selection = dialog_ui.com_list.currentText()
        is_auto_reconnect = dialog_ui.is_auto_reconnect.isChecked()

        logger.warning(f"select config {config_selection=} "
                       f"auto reconnect {is_auto_reconnect}"
                       f"{self.gcs_config.get(config_selection, None)}")

        if config_selection == "test":
            self.__test__()

        else:
            cfg = self.gcs_config.get(config_selection, None)

            if cfg is None:
                logger.critical(f"target config not exist")
                exit(250)

            _ci_core = MJWebAPIClient(is_auto_reconnect)
            ret = _ci_core.connect_remote(cfg["dev"])

            if ret:
                logger.warning(f"connect to aircraft successful.")
                self.statusbar.showMessage("connect to aircraft success", 5000)
                self.console_display.insertPlainText(START_UP)
                _ci_core.start()
                self._ci_core = _ci_core
                self._ci_timer.start(20)
                self.connected_to_fc.emit()
            else:
                logger.error(f"connect to aircraft failed.")
                self._ci_core = None

    @Slot()
    def check_rpc_result(self):
        """
        集中处理 RPC 调用的返回结果
        """
        if self._ci_core is not None:
            if self._ci_core.status != client_core.TELE_CONNECTED:
                return

            try:
                res_obj: dict = self._ci_core.receive_a_result(blocking=False)
                logger.debug(res_obj)

                # 返回的消息必须有 ID
                if not (("id" in res_obj) and ("result" in res_obj)):
                    return

                # 存在处理回调的交给处理回调处理，不存在的打印出来
                if (_res_id := res_obj.get("id")) in self._message_handler:
                    self._message_handler.get(_res_id)(res_obj)
                else:
                    formatted_json = json.dumps(res_obj, indent=2)
                    self.console_display.insertPlainText(f"{formatted_json}\n")
                    self.console_display.moveCursor(QtGui.QTextCursor.MoveOperation.End,
                                                    QtGui.QTextCursor.MoveMode.MoveAnchor)
            except queue.Empty:
                pass


if __name__ == '__main__':
    app = MengjiGCSApp()

    app.mainloop()
